package cn.lg.soar.system.biz.filetemplate;

import cn.lg.soar.common.algorithm.HmacSigner;
import cn.lg.soar.common.algorithm.Signer;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.FileTypeUtils;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.RandomUtil;
import cn.lg.soar.common.util.data.SecureRandomUtil;
import cn.lg.soar.mvc.util.ResponseUtils;
import cn.lg.soar.system.api.constant.Const;
import cn.lg.soar.system.biz.filetemplate.model.PartInfoVO;
import cn.lg.soar.system.biz.modules.general.entity.FileStorage;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * @author luguoxiang 469599794@qq.com
 * @Date: 2024/3/1 17:25
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
@Slf4j
public class LocalFileTemplateImpl implements FileTemplate {

    public final Path rootPath;
    private final int configVersion;
    private final int id;
    private final Signer signer;
    private final String baseUrl;
    private final String signUrl;
    private final boolean checkToken;

    public LocalFileTemplateImpl(FileStorage storage) {
        // 必须是服务器有的目录，防止随意新增目录
        this.rootPath = Paths.get(storage.getBucketName()).toAbsolutePath().normalize();
        File file = this.rootPath.toFile();
        AssertUtil.isTrue(file.exists(), "本地仓库目录不存在，请确认路径是否正确");
        // 需在服务器目录下放置校验文件，只有存在校验文件才能使用目录
        File checkFile = new File(file.getPath() + "/" + storage.getAccessKey());
        AssertUtil.isTrue(checkFile.exists(), "本地仓库校验失败，请在根路径下放置名为 " + storage.getAccessKey() + " 的文件");

        this.baseUrl = storage.getEndpoint() + "/" + Const.LOCAL_FILE_PATH + "/%d/%s";
        this.signUrl = baseUrl + "?$=%s.%s.%s";
        this.configVersion = storage.getVersion();
        this.id = storage.getId();
        String secretKey = storage.getSecretKey();
        if (DataUtil.isValuable(secretKey)) {
            this.signer = HmacSigner.sha256("LOC:" + storage.getSecretKey());
            this.checkToken = true;
        } else {
            this.signer = HmacSigner.sha256("LOC:" + RandomUtil.getString(16));
            this.checkToken = false;
        }
    }

    @Override
    public void upload(String filename, InputStream stream) {
        Path targetFile = getFilePath(filename);
        try {
            // 创建目录
            Path targetDir = targetFile.getParent();
            if (!Files.exists(targetDir)) {
                Files.createDirectories(targetDir);
            }
            // 上传文件
            try (InputStream safeStream = stream) {
                Files.copy(safeStream, targetFile, StandardCopyOption.REPLACE_EXISTING);
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to upload file: " + filename, e);
        }
    }

    @Override
    @SneakyThrows
    public InputStream getInputStream(String filename) {
        Path filePath = getFilePath(filename);
        File file = filePath.toFile();
        if (!file.exists()) {
            throw new AmazonS3Exception("The specified key does not exist: " + filename);
        }
        return Files.newInputStream(file.toPath());
    }

    @Override
    @SneakyThrows
    public S3Object getObject(String filename) {
        Path filePath = getFilePath(filename);
        File file = filePath.toFile();
        if (!file.exists()) {
            throw new AmazonS3Exception("The specified key does not exist: " + filename);
        }

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(FileTypeUtils.getContentType(file));
        metadata.setContentLength(file.length());

        InputStream inputStream;
        try {
            inputStream = Files.newInputStream(file.toPath());
        } catch (IOException e) {
            throw new AmazonS3Exception("Failed to open input stream for file: " + filename, e);
        }

        S3Object s3Object = new S3Object();
        s3Object.setObjectContent(inputStream);
        s3Object.setObjectMetadata(metadata);
        return s3Object;
    }

    @Override
    @SneakyThrows
    public void deleteFile(String filename) {
        Path filePath = getFilePath(filename);
        Files.deleteIfExists(filePath);
    }

    @Override
    public boolean isExist(String filename) {
        Path filePath = getFilePath(filename);
        return filePath.toFile().exists();
    }

    @Override
    public URL getUrl(String filename) {
        try {
            return new URL(String.format(baseUrl, this.id, filename));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public URL getAccessUrl(String filename, Date date) {
        long expiresAt = System.currentTimeMillis() + 3600_000;
        String random = SecureRandomUtil.getString(16);
        String sign = sign(filename, HttpMethod.GET, expiresAt, random);
        // 构造本地模拟 URL
        try {
            return new URL(String.format(signUrl, this.id, filename, expiresAt, random, sign));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String getUploadUrl(String filename) {
        long expiresAt = System.currentTimeMillis() + 2 * 3600 * 1000;
        String random = SecureRandomUtil.getString(16);
        String sign = sign(filename, HttpMethod.POST, expiresAt, random);
        // 构造本地模拟 URL
        return String.format(signUrl, this.id, filename, expiresAt, random, sign);
    }

    @Override
    public String getUploadUrl(String filename, String uploadId, Integer partNumber) {
        filename = uploadId + "/" + partNumber;
        long expiresAt = System.currentTimeMillis() + 2 * 3600 * 1000;
        String random = SecureRandomUtil.getString(16);
        String sign = sign(filename, HttpMethod.POST, expiresAt, random);
        // 构造本地模拟 URL
        return String.format(signUrl, this.id, filename, expiresAt, random, sign);
    }

    @Override
    public String uploadInit(String filename) {
        String uploadId = UUID.randomUUID().toString();
        Path filePath = getFilePath(uploadId + "/");
        AssertUtil.isTrue(filePath.toFile().mkdirs(), "Failed to create directory: " + filePath.toAbsolutePath());
        return uploadId;
    }

    @Override
    public void uploadComplete(String filename, String uploadId, List<PartInfoVO> parts) {
        if (parts.isEmpty()) {
            throw new IllegalArgumentException("No parts to merge");
        }

        Path finalPath = getFilePath(filename);
        Path tmpPath = getFilePath(filename + ".tmp");
        Path tempDir = getFilePath(uploadId + "/");

        // 安全校验（示例）
        if (!isValidFilename(filename)) {
            throw new IllegalArgumentException("Invalid filename");
        }

        parts.sort(Comparator.comparing(PartInfoVO::getPartNumber));
        try {
            Files.createDirectories(finalPath.getParent());
            Files.createDirectories(tmpPath.getParent());
        } catch (IOException e) {
            throw new RuntimeException("Failed to create parent directories for: " + filename, e);
        }
        try {
            try (FileChannel outChannel = FileChannel.open(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
                for (PartInfoVO part : parts) {
                    Path partFile = tempDir.resolve(String.valueOf(part.getPartNumber()));
                    if (!Files.exists(partFile)) {
                        throw new IllegalStateException("Missing part file: " + partFile);
                    }
                    try (FileChannel inChannel = FileChannel.open(partFile, StandardOpenOption.READ)) {
                        long position = 0;
                        long size = inChannel.size();
                        while (position < size) {
                            long transferred = inChannel.transferTo(position, size - position, outChannel);
                            position += transferred;
                        }
                    }
                }
            }

            // 原子替换
            Files.move(tmpPath, finalPath, StandardCopyOption.REPLACE_EXISTING);

            // 清理临时目录
            deleteRecursively(tempDir);

        } catch (IOException e) {
            // 清理中间文件
            try { Files.deleteIfExists(tmpPath); } catch (IOException ignored) {}
            try { deleteRecursively(tempDir); } catch (Exception ignored) {}
            throw new RuntimeException("Failed to complete upload: " + filename, e);
        }
    }

    private void deleteRecursively(Path dir) throws IOException {
        if (!Files.exists(dir)) return;
        Files.walk(dir)
                .sorted(Comparator.reverseOrder())
                .forEach(path -> {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        System.err.println("Warning: failed to delete " + path + ": " + e.getMessage());
                    }
                });
    }

    private boolean isValidFilename(String name) {
        return name != null && name.matches("[/a-zA-Z0-9._-]+");
    }

    @Override
    public void uploadAbort(String filename, String uploadId) {
        Path tempDir = getFilePath(filename + "/");
        // 删除临时目录
        try {
            Files.walk(tempDir)
                    .sorted(Comparator.reverseOrder())
                    .forEach(path -> {
                        try { Files.delete(path); } catch (IOException ignored) {}
                    });
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public int getConfigVersion() {
        return configVersion;
    }

    @Override
    public void test() {}

    /**
     * 下载文件
     * @param filename
     * @param token
     * @return
     */
    public void download(String filename, String token) {
        if (this.checkToken) {
            // 安全验证
            String[] split = token.split("\\.", 3);
            long expiresAt = Long.parseLong(split[0]);
            AssertUtil.isTrue(expiresAt > System.currentTimeMillis(), "签名失效");
            String sign = sign(filename, HttpMethod.GET, expiresAt, split[1]);
            AssertUtil.isTrue(sign.equals(split[2]), "无效token");
        }
        download(filename);
    }

    public void download(String filename) {
        // 构建完整文件路径
        Path filePath = getFilePath(filename);
        ResponseUtils.responseFile(filePath, filename);
    }

    /**
     * 上传文件
     * @param file
     * @param filename
     * @param token
     * @return
     * @throws IOException
     */
    public String upload(MultipartFile file, String filename, String token) throws IOException {
        if (this.checkToken) {
            // 安全验证
            String[] split = token.split("\\.", 3);
            long expiresAt = Long.parseLong(split[0]);
            AssertUtil.isTrue(expiresAt > System.currentTimeMillis(), "签名失效");
            String sign = sign(filename, HttpMethod.POST, expiresAt, split[1]);
            AssertUtil.isTrue(sign.equals(split[2]), "无效token");
        }
        // Step 4: 保存文件到本地
        try (InputStream inputStream = file.getInputStream()) {
            upload(filename, inputStream);
        }
        return String.format(baseUrl, this.id, filename);
    }

    public String getEtag(String filename) {
        Path filePath = getFilePath(filename);
        try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }

            byte[] digest = md.digest();
            BigInteger bigInt = new BigInteger(1, digest);
            String hashText = bigInt.toString(16);

            // 转成 32 位小写字符串，补零
            while (hashText.length() < 32) {
                hashText = "0" + hashText;
            }

            return "\"" + hashText + "\"";
        } catch (NoSuchAlgorithmException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String sign(String filename, HttpMethod method, long expiresAt, String random) {
        String format = String.format("%s/%s/%s/%s", filename, method.name(), expiresAt, random);
        return signer.signBase64(format);
    }

    private Path getFilePath(String filename) {
        // 防止路径穿越
        Path targetFile = this.rootPath.resolve(filename).normalize();
        if (!targetFile.startsWith(this.rootPath)) {
            throw new SecurityException("Invalid filename: path traversal attempt detected.");
        }
        return targetFile;
    }

}